package guiservice.client;

import com.esotericsoftware.kryonet.Client;
import com.esotericsoftware.kryonet.Connection;
import com.esotericsoftware.kryonet.Listener;
import com.esotericsoftware.kryonet.rmi.ObjectSpace;
import com.esotericsoftware.kryonet.rmi.RemoteObject;
import guiservice.common.controls.*;
import guiservice.common.controls.handlers.KeyEventArgs;
import guiservice.common.controls.handlers.MouseEventArgs;
import guiservice.common.net.ProtocolRegistration;
import guiservice.common.net.Server;
import lombok.Getter;
import lombok.extern.log4j.Log4j;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;

@Log4j
public class ClientImpl implements guiservice.common.net.Client {

    private final String HOST = "127.0.0.1";
    private final int PORT = 6666;

    private final Client client;
    private final ObjectSpace clientObjectSpace = new ObjectSpace();
    @Getter
    private Server serverImpl;
    private Connection singleConnection;
    @Getter
    private long uid = -1;
    private final ConcurrentHashMap<Long, Control> mapIdToControl = new ConcurrentHashMap<>();

    ClientImpl(final Application app) {
        client = new Client();
        ProtocolRegistration.registerDataObjects(client);
        clientObjectSpace.setExecutor(Executors.newCachedThreadPool());
        clientObjectSpace.register(OBJECT_ID, this);
        client.addListener(new Listener.ThreadedListener(new Listener() {

            @Override
            public void connected(Connection connection) {
                super.connected(connection);
                singleConnection = connection;
                log.info(String.format("Connected to address %s", connection.getEndPoint()));
                clientObjectSpace.addConnection(connection);

                serverImpl = ObjectSpace.getRemoteObject(connection, guiservice.common.net.Server.OBJECT_ID, Server.class);
                RemoteObject remoteObject = (RemoteObject) serverImpl;
                remoteObject.setNonBlocking(false);
                remoteObject.setTransmitReturnValue(true);
                remoteObject.setTransmitExceptions(false);
            }

            @Override
            public void disconnected(Connection connection) {
                super.disconnected(connection);
                log.info(String.format("Disconnected from address %s", connection.getEndPoint()));
                serverImpl = null;
                singleConnection = null;
                mapIdToControl.clear();
                clientObjectSpace.remove(Server.OBJECT_ID);
                clientObjectSpace.removeConnection(connection);
                app.close();
            }

            public void received(Connection connection, Object object) {
                if (object instanceof Long) {
                    uid = (long) object;
                }
            }
        }));
        client.start();
    }

    void start() throws IOException {
        client.connect(10000, HOST, PORT);
    }

    void stop() {
        try {
            for (Control c : mapIdToControl.values()) {
                RemoteObject cRemote = (RemoteObject) c;
                cRemote.close();
            }
            if (serverImpl != null) {
                serverImpl.unregisterControls(mapIdToControl.keySet().toArray(new Long[0]));
                RemoteObject serverImplRemote = (RemoteObject) serverImpl;
                serverImplRemote.close();
            }
            clientObjectSpace.close();
        } catch (Exception e) {
            log.error("Error occurred while trying to close all remote objects ", e);
        }

        if (client.isConnected()) {
            client.close();
        }
    }

    <T extends Control> T createAndGetRemoteObject(long ownerId, String className, Class<T> resultType) {
        if (serverImpl != null && singleConnection != null) {
            log.debug(String.format("try registerControl ownerId=%d, className=%s, uid=%d", ownerId, className, uid));
            long id = serverImpl.registerControl(ownerId, className, uid);
            return getControl(id, resultType, true);
        }
        return null;
    }

    @Override
    public void eventHandler(long objectId, MouseEventArgs args) {
        log.debug(String.format("id=%d, %s", objectId, args));
        try {
            Control control = mapIdToControl.get(objectId);
            EventHandler.getInstance().handleEvent(control, args);
        } catch (Exception e) {
            log.error("Error occurred while processing event", e);
        }
    }

    @Override
    public void eventHandler(long objectId, KeyEventArgs args) {
        log.debug(String.format("id=%d, %s", objectId, args));
        Control control = mapIdToControl.get(objectId);
        EventHandler.getInstance().handleEvent(control, args);
    }

    public <T extends Control> T getControl(long id, Class<T> clazz, boolean isMine) {
        if (serverImpl != null && singleConnection != null) {
            log.debug(String.format("try getControl id=%d, resultType=%s", id, clazz));
            T result = ObjectSpace.getRemoteObject(singleConnection, (int)id, clazz);
            RemoteObject remoteObject = (RemoteObject) result;
            remoteObject.setNonBlocking(false);
            remoteObject.setTransmitReturnValue(true);
            remoteObject.setTransmitExceptions(false);
            if (isMine) {
                mapIdToControl.put(id, result);
            }
            return result;
        }
        return null;
    }

    public <T extends Control> List<T> getControls(long uid, Class<T> cls) {
        Long[] controls = serverImpl.getControls(uid, cls.getSimpleName());
        List<T> result = new ArrayList<>();
        for (int i = 0; i < controls.length; i++) {
            result.add(getControl(controls[i], cls, false));
        }
        return result;
    }
}
